#include <linux/module.h>
#include <linux/init.h>
#include <linux/ioctl.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/kernel.h>
#include <linux/errno.h>

#include <asm/system.h>
#include <asm/uaccess.h>

#include <linux/proc_fs.h>

MODULE_LICENSE("GPL");

#include "mmdisk.h"

static int mmdisk_major, mmdisk_minor;

static struct mmdisk_dev *pMDEV;

static struct proc_dir_entry *pde_dir;
static struct proc_dir_entry *pde_data;

static struct mmdisk_qset* alloc_mmqset()
{
	int i;
	struct mmdisk_qset * pqset;
	
	printk(KERN_INFO "alloc_mmqset\n");
	pqset = kmalloc(sizeof(*pqset), GFP_KERNEL);
	pqset->pnext = NULL;
	pqset->npos = 0;
	pqset->size = 0;
	
	for (i = 0; i < MAX_ITEM_NUM; i++) {
		pqset->pitem[i] = (char *)kmalloc(MAX_ITEM_SIZE, GFP_KERNEL);
		memset(pqset->pitem[i], 0, sizeof(MAX_ITEM_SIZE));
	}
	
	return pqset;
}

static int delloc_mmqset(struct mmdisk_qset *pqset)
{
	int i;
	if (pqset == NULL) {
		printk(KERN_WARNING "Warning: pqset is NULL\n");
		return -1;
	}
	
	for (i = 0; i < MAX_ITEM_NUM; i++) {
		if (pqset->pitem[i] != NULL) {
			kfree(pqset->pitem[i]);
		}
	}
	
	return 0;
}



struct mmdisk_qset *mmdisk_follow(int qset_num)
{
	struct mmdisk_qset *qset = pMDEV->phead;
	if (qset == NULL) {
		return NULL;
	}
	while (qset_num) {
		qset = qset->pnext;
		if (qset == NULL)
			return NULL;
		qset_num--;
	}

	return qset;
}

struct mmdisk_qset *mmdisk_wt_follow(int qset_num)
{
	struct mmdisk_qset *qset = pMDEV->phead;
	struct mmdisk_qset *qset_pre;
	if (qset == NULL) {
		alloc_mmqset(qset);
	}
	while (qset_num) {
		qset_pre = qset;
		qset = qset->pnext;
		if (qset == NULL) {
			qset = alloc_mmqset();
			qset_pre->pnext = qset;
		}
		qset_num--;
	}

	printk(KERN_WARNING "mmdisk_wt_follow: qset = %d\n", qset);
	return qset;
}


int mmdisk_open(struct inode *inode, struct file *filp)
{
	struct mmdisk_dev *dev;
	
	dev = container_of(inode->i_cdev, struct mmdisk_dev, cdev);
	filp->private_data = dev;
	
	printk(KERN_WARNING "Info: open success\n");
	
	return 0;
}


ssize_t mmdisk_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
	printk(KERN_WARNING "Info: read\n");
	printk(KERN_WARNING "Info: count = %d, pos = %d\n", count, *f_pos);


	struct mmdisk_dev *mdev = filp->private_data;
	struct mmdisk_qset *qset;
	size_t num_st, item_st, offset_st, tmp;
	size_t num_ed, item_ed, offset_ed;
	int retval = 0;
	int i, j;
	size_t user_pos = 0;
	size_t user_cnt = 0;

	if (*f_pos > pMDEV->totalsize) {
		goto out;
	}

	if (*f_pos + count > pMDEV->totalsize) {
		count = pMDEV->totalsize - *f_pos;
	}

	printk(KERN_WARNING "Info: count = %d, pos = %d\n", count, *f_pos);

	num_st = *f_pos / (MAX_ITEM_NUM * MAX_ITEM_SIZE);
	tmp = *f_pos % (MAX_ITEM_NUM * MAX_ITEM_SIZE);
	item_st = tmp / MAX_ITEM_SIZE;
	offset_st = tmp % MAX_ITEM_NUM;

	num_ed = (*f_pos + count) / (MAX_ITEM_NUM * MAX_ITEM_SIZE);
	tmp = (*f_pos + count) % (MAX_ITEM_NUM * MAX_ITEM_SIZE);
	item_ed = tmp / MAX_ITEM_SIZE;
	offset_ed = tmp % MAX_ITEM_NUM;

	qset = mmdisk_follow(num_st);

	if (num_st == num_ed) {
		if (item_st == item_ed) { 
			copy_to_user(buf, (char*)(qset->pitem[item_st]) + offset_st, count);
			user_pos += count;
		}
		else {
			for (j = item_st; j <= item_ed; j++) {
				if (j == item_st) {
					copy_to_user(buf, (char*)(qset->pitem[j]) + offset_st, MAX_ITEM_SIZE - offset_st);
					user_pos += MAX_ITEM_SIZE - offset_st;
				} else if (j == item_ed) {
					copy_to_user(buf + user_pos, qset->pitem[j], offset_ed);
					user_pos += offset_ed;
				} else {
					copy_to_user(buf + user_pos, qset->pitem[j], MAX_ITEM_SIZE);
					user_pos += MAX_ITEM_SIZE;
				}
			}

		}


	} else {
		for (i = num_st; i <= num_ed; i ++) {
			if (qset == NULL) {
				printk(KERN_WARNING "qset NULL: %d\n", i);
				goto out;

			}
			for (j = item_st; j <= item_ed; j++) {
				if (i == num_st && j == item_st) {
					copy_to_user(buf, (char*)(qset->pitem[item_st]) + offset_st, count);
					user_pos += count;
				} else if (i == num_ed && j == item_ed) {
					copy_to_user(buf + user_pos, qset->pitem[j], offset_ed);
					user_pos += offset_ed;
				} else {
	
					copy_to_user(buf + user_pos, qset->pitem[j], MAX_ITEM_SIZE);
					user_pos += MAX_ITEM_SIZE;
				}
			}
			qset = qset->pnext;
		}
	}

	if (retval >= 0)
		retval = user_pos;
out:
	printk(KERN_INFO "retval = %d\n", retval);
	return retval;
}

ssize_t mmdisk_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
	printk(KERN_WARNING "Info: write\n");
	printk(KERN_WARNING "Info: count = %d, pos = %d\n", count, *f_pos);
	
	struct mmdisk_dev *mdev = filp->private_data;
	struct mmdisk_qset *qset, *qset_pre;
	size_t num_st, item_st, offset_st, tmp;
	size_t num_ed, item_ed, offset_ed;
	int retval = -1;
	int i, j;
	size_t user_pos = 0;
	size_t user_cnt = 0;

	num_st = *f_pos / (MAX_ITEM_NUM * MAX_ITEM_SIZE);
	tmp = *f_pos % (MAX_ITEM_NUM * MAX_ITEM_SIZE);
	printk(KERN_INFO "tmp = %d\n", tmp);
	item_st = tmp / MAX_ITEM_SIZE;
	offset_st = tmp % MAX_ITEM_SIZE;

	num_ed = (*f_pos + count) / (MAX_ITEM_NUM * MAX_ITEM_SIZE);
	tmp = (*f_pos + count) % (MAX_ITEM_NUM * MAX_ITEM_SIZE);
	printk(KERN_INFO "tmp = %d\n", tmp);
	item_ed = tmp / MAX_ITEM_SIZE;
	offset_ed = tmp % MAX_ITEM_SIZE;

	if (num_st >= MAX_QSET_NUM || num_ed >= MAX_QSET_NUM) {
		printk(KERN_WARNING "write execd, num_st = %d, num_ed = %d\n", num_st, num_ed);
		goto out;
	}

	printk(KERN_WARNING "num_st = %d, item_st = %d, offset_st = %d, num_ed = %d, item_ed = %d, offset_ed = %d\n", num_st, item_st, offset_st, num_ed, item_ed, offset_ed);

	qset = mmdisk_wt_follow(num_st);

	if (num_st == num_ed) {
		if (item_st == item_ed) { 
			copy_from_user((char*)(qset->pitem[item_st]) + offset_st, buf, count);
			user_pos += count;
			pMDEV->totalsize += count;
		}
		else {
			for (j = item_st; j <= item_ed; j++) {
				if (j == item_st) {
					copy_from_user((char*)(qset->pitem[j]) + offset_st, buf, MAX_ITEM_SIZE - offset_st);
					user_pos += MAX_ITEM_SIZE - offset_st;
					pMDEV->totalsize += MAX_ITEM_SIZE - offset_st;
				} else if (j == item_ed) {
					copy_from_user(qset->pitem[j], buf + user_pos, offset_ed);
					user_pos += offset_ed;
					pMDEV->totalsize += offset_st;
				} else {
					copy_from_user(qset->pitem[j], buf + user_pos, MAX_ITEM_SIZE);
					user_pos += MAX_ITEM_SIZE;
					pMDEV->totalsize += MAX_ITEM_SIZE;
				}
			}

		}


	} else {
		for (i = num_st; i <= num_ed; i ++) {
			for (j = item_st; j <= item_ed; j++) {
				if (i == num_st && j == item_st) {
					copy_from_user((char*)(qset->pitem[item_st]) + offset_st, buf, count);
					user_pos += count;
					pMDEV->totalsize += count;
				} else if (i == num_ed && j == item_ed) {
					copy_from_user(qset->pitem[j], buf + user_pos, offset_ed);
					user_pos += offset_ed;
					pMDEV->totalsize += offset_ed;
				} else {
	
					copy_from_user(qset->pitem[j], buf + user_pos, MAX_ITEM_SIZE);
					user_pos += MAX_ITEM_SIZE;
					pMDEV->totalsize += MAX_ITEM_SIZE;
				}
			}
			qset_pre = qset;
			qset = qset->pnext;
			if (qset == NULL) {
				qset = alloc_mmqset();
				qset_pre->pnext = qset;
			}
		}
	}

	retval = user_pos;
	
out:
	printk(KERN_WARNING "write return: %d\n", retval);
	return retval;
	
}

int mmdisk_ioctl(struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg)
{

	return 0;
}

int mmdisk_release(struct inode *inode, struct file *filp)
{

	return 0;
}


static struct file_operations mmdisk_fops = {
	.owner =	THIS_MODULE,
	.read = 	mmdisk_read,
	.write = 	mmdisk_write,
	.ioctl = 	mmdisk_ioctl,
	.open = 	mmdisk_open,
	.release = 	mmdisk_release,

};
static int mmdisk_setup_cdev(struct mmdisk_dev *dev, int index)
{
	int err, devno = MKDEV(mmdisk_major, mmdisk_minor);
	cdev_init(&dev->cdev, &mmdisk_fops);
	dev->cdev.owner = THIS_MODULE;
	dev->cdev.ops = &mmdisk_fops;
	err = cdev_add(&dev->cdev, devno, 1);
	if (err) {
		printk(KERN_NOTICE "Error %d adding mmdisk %d\n" ,err, index);
	}

	return 0;
	
}

int proc_read(char *page, char **start, off_t off, int count, int *eof, void *data)
{
	printk(KERN_INFO "enter proc_readm off = %d, count = %d\n", off, count);
	int data_len = 0;
	data_len = sprintf(page, "%d\n", pMDEV->totalsize);
	data_len -= off;
	if (data_len < 0)
		data_len = 0;
	*eof = (data_len <= count) ? 1 : 0;
	*start = page + off;
	
	return data_len;
}

int proc_write(char *page, char **start, off_t off, int count, int *eof, void *data)
{
	return 0;
	
}

static int mmdisk_init(void)
{
	int err;
	dev_t dev;
	char *devname = "mmdisk-test";

	err = alloc_chrdev_region(&dev, 0, 1, devname);
	if (err) {
		printk(KERN_ERR "Error: alloc_chrdev_region failed\n");
		return -1;
	}

	mmdisk_major = MAJOR(dev);
	mmdisk_minor = MINOR(dev);
	printk(KERN_WARNING "Warning: mmdisk_major = %d\n", mmdisk_major);
	
	if (pMDEV != NULL) {
		printk(KERN_WARNING "Warning: pmdev not NULL\n");
	}

	pMDEV = kmalloc(sizeof(*pMDEV), GFP_KERNEL);
	pMDEV->phead = NULL;
	pMDEV->totalsize = 0;
	init_MUTEX(&pMDEV->sem);
	mmdisk_setup_cdev(pMDEV, 0);
	pMDEV->phead = alloc_mmqset();

	pde_dir = proc_mkdir("mmdisk", NULL);
	pde_data = create_proc_entry("status", 0644, pde_dir);
	pde_data->data		= pMDEV;
	pde_data->read_proc	= proc_read;

	printk(KERN_WARNING "Info: mmdisk init\n");

	return 0;
}

static void mmdisk_exit(void)
{

	dev_t dev = MKDEV(mmdisk_major, mmdisk_minor);

	struct mmdisk_qset *pH, *pT;
	if (pMDEV == NULL) {
		printk(KERN_WARNING "Warning: pMDEV is NULL\n");
		return;
	}
	pH = pMDEV->phead;
	while (pH) {
		pT = pH->pnext;
		delloc_mmqset(pH);
		pH = pT;
	}
	kfree(pMDEV);

	remove_proc_entry("status", pde_dir);
	remove_proc_entry("mmdisk", NULL);
	unregister_chrdev_region(dev, 1);
	printk(KERN_WARNING "Info: mmdisk exit\n");
	return;
}


module_init(mmdisk_init);
module_exit(mmdisk_exit);
